package edu.uoc.sd.fr14.impl;

import edu.uoc.sd.fr14.actors.Cluster;
import edu.uoc.sd.fr14.imesg.*;
import edu.uoc.sd.fr14.model.*;

/**
 * @author Jordi Caballer Paez [jcaballerp@uoc.edu]
 * @author Emilio Molina Haro [emiliew@uoc.edu]
 */
public class StudentServer extends AbstractServer{
	
	private Cluster cluster;
	private Log log;
	private NseqTracker nseq;
	private Role rol;
	private ServerId id;
	private Company company;

	public StudentServer(ServerState serverState, Cluster cluster) {
		
		super(serverState, cluster);
		
		/* 	I get all the attributes and I make them own. Memory position keeps the same,
		 * so we are working directly on the serverState: */
		this.cluster = cluster;
		log = serverState.getLog();
		nseq = serverState.getNseqTracker();
		rol = serverState.getRole();
		company = serverState.getCompany();
		id = rol.getMyId();

	}

	@Override
	public void abort() {
		/* If we receive an abort from any server during an operation or election
		 * we first cancel all the operations and elections. Then, if the server
		 * is the leader, leader is removed. */
		log.cancelOps();
		rol.cancelElections();
		if(rol.isLeader())
			rol.removeLeader();
	}

	@Override
	public void apply(Nseq nseq, SubLog ops, ServerId from) {
		/* Once the apply op. is received from the leader, we check is it is the
		 * expected nseq or, otherwise, we need to recover previous operations. */
		if(this.nseq.isAccepted(nseq, from)){
			/* If the nseq is the expected, operation is applied and nseq advanced. */
			
			ops.apply(company);
			this.nseq.advance(ops, from);	
		}
		else
			/* If the nseq is no the expected, we recover the operations since the
			 * last commited by that server. */
			cluster.sendToServer(new RecoverMessage(from, this.nseq.getCurrent(from),false, id));
	}

	@Override
	public void confirm(FlightId flight, CustomerId customer) {
		/*We check if the customer has a seat in the flight and give back the answer: */
		Flight fl= company.getFlight(flight);
		boolean confirmed= fl.confirm(customer);
		cluster.sendToCustomer(new ConfirmedMessage(customer, flight, customer, confirmed));	
	}

	@Override
	public void elect(ServerId from) {
		/* Only if the id of this server is higher, it will send an abort message and it 
		 * will send elect messages to every server with higher id than itself. */
		if(id.isGreaterThan(from)){
			cluster.sendToServer(new AbortMessage(from));
		
			for(ServerId target : cluster.getServers())
				if(target.isGreaterThan(id))
					cluster.sendToServer(new ElectMessage(target, id));
			/* A new election will be set only if there is no current election going on: */
			if(!rol.isDoingElections()){
				cluster.getClock().setElectionTo(id);
				rol.beginElections();
			}
		}
	}

	@Override
	public void prepare(OpId op, ServerId from) {
		
		if(rol.isLeader()){
			/* In case server is the leader, there is a conflict, an AbortMessage is sent
			 * and new elections begin: */
			cluster.sendToServer(new AbortMessage(from));
			
			for(ServerId target : cluster.getServers())
				if(target.isGreaterThan(id))
					cluster.sendToServer(new ElectMessage(target, id));
			/* A new election will be set only if there is no current election going on: */
			if(!rol.isDoingElections()){
				cluster.getClock().setElectionTo(id);
				rol.beginElections();
			}
			
		}else{
			/* In case the server is a follower or the leader is unknown, leader is set and
			 * a PreparedMessage is sent. */
			rol.setLeader(from);
			cluster.sendToServer(new PreparedMessage(from, op));
		}
	}

	@Override
	public void prepared(OpId op) {
		/* We first check if the server is still the leader: */
		if (rol.isLeader()){ 
			/* We check if the quorum is reached. Only if it is reached (not overcome),
			 * operacion comes not empty and, thus, operation is done. */
			Op operacion = log.prepared(op, cluster.getQuorum());
			
			/* If quorum has been reached, operacion will not be empty: */
			if(operacion != null){

					/* We cancel the clock counter. Not after, because it can reach timeout. */
					cluster.getClock().cancelPrepareTo(op, id);
					/* Operation is executed in the own server. */
					Nseq nsq = log.executeOp(operacion, company);
					
					/* We check if the operation has been successful. */
					if(nsq != null){
						/* We create the set of operations from the current operation.*/
						SubLog ops = log.recover(nsq);
						/* Leader tells the rest of the servers to apply the changes: */
						for(ServerId target : cluster.getServers())
							if (!id.equals(target))
								cluster.sendToServer(new ApplyMessage(target, nsq, ops, id));
					}
					/* After all, a message is returned to the customer informing if he/she has a seat or not. */
					boolean confirmado = (company.getFlight(operacion.getFlight()).confirm(operacion.getCustomer()));
					if (confirmado)
						cluster.sendToProxy(new ConfirmedMessage(operacion.getProxy(), operacion.getFlight(), operacion.getCustomer(), true));
					else
						cluster.sendToProxy(new ConfirmedMessage(operacion.getProxy(), operacion.getFlight(), operacion.getCustomer(), false));	
			}		
		}		
	}

	@Override
	public void query(FlightId flight, CustomerId from) {
		cluster.sendToCustomer(new QueriedMessage(from, flight, company.getFlight(flight).query()));
	}

	@Override
	public void seat(FlightId flight, CustomerId customer, ProxyId from) {
		
		/* If server is the leader... */
		if(rol.isLeader()){	
			
			/* Annotate to get the operation id to get its id: */
			OpId opId = log.annotate(flight, customer, from);
			
			boolean libres = company.getFlight(flight).query() > 0;
			boolean confirmado = company.getFlight(flight).confirm(customer);
			
			/* We set the clock, in order to know when the timeout is reached. */
			cluster.getClock().setPrepareTo(opId, id);
			if(!confirmado && libres){
				/* PrepareMessages are sent to every server excepting this one. */
				for(ServerId target : cluster.getServers())
					if (!id.equals(target))
						cluster.sendToServer(new PrepareMessage(target, opId, id));
				
			}else{
				
				/* We cancel the prepareTimeout: */
				cluster.getClock().cancelPrepareTo(opId, id);
				
				if(confirmado)
					cluster.sendToProxy(new ConfirmedMessage(from, flight, customer, true));
				else
					cluster.sendToProxy(new ConfirmedMessage(from, flight, customer, false));
			}
				

		}else{
			/* We set the seatTimeout to reach to elections only if there are not
			 * elections currently: */
			if(!rol.isDoingElections())
				cluster.getClock().setSeatTo(flight, customer, id);
			
			if(rol.isLeaderKnown())
				/* If leader is known,  message is forwarded to it: */
				cluster.sendToServer(new SeatMessage(rol.getLeader(), flight, customer, id));
			else
				/* If leader is not known, we confirm the seat with false: */
				cluster.sendToProxy(new ConfirmedMessage(from, flight, customer, false));			
		}
	}

	@Override
	public void recover(Nseq nseq, boolean leader, ServerId from) {
		/* We first give back the operations that were required: */
		SubLog ops = log.recover(nseq);
		cluster.sendToServer(new ApplyMessage(from, nseq, ops, id));
		
		/* If boolean leader is true, from server is the new leader */
		if(leader){
			if(!rol.isLeader())
				/*If this server is not leader, we just set the new leader.*/
				rol.setLeader(from);
			else{
				/* If this server is leader, we send an abort message and elect messages to the 
				 * higher id servers starting a new election or keeping on an existing one: */
				cluster.sendToServer(new AbortMessage(from));
				for(ServerId target : cluster.getServers())
					if(target.isGreaterThan(id))
						cluster.sendToServer(new ElectMessage(target, id));
				/* A new election will be set only if there is no current election going on: */
				if(!rol.isDoingElections()){
					cluster.getClock().setElectionTo(id);
					rol.beginElections();
				}
			}
		}
		
	}

	@Override
	public void timeoutElectionBegin() {
		log.cancelOps();
		if(!rol.isDoingElections()){
			/* Only if we are not doing elections, we begin new elections: */
			rol.beginElections();
			rol.removeLeader();
			//cluster.getClock().setElectionTo(id); --- Elections Timeout are already set.
			for(ServerId target : cluster.getServers())
				if(target.isGreaterThan(id))
					cluster.sendToServer(new ElectMessage(target, id));
		}
	}

	@Override
	public void timeoutElectionEnd() {
		/*This is only effective when there are elections going on: */
		if(rol.isDoingElections()){
			rol.endElections();
			rol.becomeLeader();
			synchronize();
		}
	}

	@Override
	public void synchronize() {
		for(ServerId target : cluster.getServers())
			if (!id.equals(target))
				cluster.sendToServer(new RecoverMessage(target, nseq.getCurrent(target), rol.isLeader(), id));
	}

	@Override
	public void confirmed(FlightId flight, CustomerId customer, boolean value) {
		/* We cancel a possible existing timeout for seat operation committed to a follower. 
		 * Nothing happens if we cancel a non-existing timer: */
		cluster.getClock().cancelSeatTo(flight, customer, id);
		cluster.sendToCustomer(new ConfirmedMessage(customer, flight, customer, value));
	}

    
}
